﻿#region License
/* Copyright (C) 2009 Tim Coulter
 * 
 * This file is part of ExtremeML.
 * 
 * ExtremeML is free software: you can redistribute
 * it and/or modify it under the terms of the GNU General Public
 * License, as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 * 
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty
 * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
 * the GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see http://www.gnu.org/licenses/.
*/
#endregion

using System;
using System.Collections.Generic;
using System.Data;
using DocumentFormat.OpenXml.Spreadsheet;
using ExtremeML.Core;
using ExtremeML.Packaging;
using ExtremeML.Spreadsheet.Address;

namespace ExtremeML.Spreadsheet
{
	// This is a partial class that adds custom functionality to its auto-generated partner.
	public sealed partial class TableWrapper
	{
		#region Custom Properties

		/// <summary>
		/// Gets the containing <see cref="WorksheetPartWrapper"/>.
		/// </summary>
		public WorksheetPartWrapper WorksheetPart
		{
			get { return (WorksheetPartWrapper)Parent.Parent.Parent; }
		}

		/// <summary>
		/// Gets the associated <see cref="WorksheetWrapper"/>.
		/// </summary>
		public WorksheetWrapper Worksheet
		{
			get { return WorksheetPart.Worksheet; }
		}

		/// <summary>
		/// Gets the <see cref="CellArea"/> that manages range addressing for the wrapped table.
		/// </summary>
		public CellArea CellArea { get; private set; }

		/// <summary>
		/// Gets or sets the zero-based index of the first row of the wrapped table.
		/// </summary>
		public int? FirstRowIndex
		{
			get { return CellArea.BeginCell.RowIndex; }
			internal set { CellArea.BeginCell.RowIndex = value; }
		}

		/// <summary>
		/// Gets or sets the zero-based index of the first column of the wrapped table.
		/// </summary>
		public int? FirstColumnIndex
		{
			get { return CellArea.BeginCell.ColumnIndex; }
			internal set { CellArea.BeginCell.ColumnIndex = value; }
		}

		/// <summary>
		/// Gets or sets the zero-based index of the last row of the wrapped table.
		/// </summary>
		public int? LastRowIndex
		{
			get { return CellArea.EndCell.RowIndex; }
			internal set { CellArea.EndCell.RowIndex = value; }
		}

		/// <summary>
		/// Gets or sets the zero-based index of the last column of the wrapped table.
		/// </summary>
		public int? LastColumnIndex
		{
			get { return CellArea.EndCell.ColumnIndex; }
			internal set { CellArea.EndCell.ColumnIndex = value; }
		}

		/// <summary>
		/// Gets the zero-based index of the last data row of the wrapped table.
		/// </summary>
		public int? LastDataRowIndex
		{
			get { return (int)(LastRowIndex - (TotalsRowCount ?? 0)); }
		}

		/// <summary>
		/// Gets the number of data rows in the wrapped table (excluding mandatory header and optional total rows).
		/// </summary>
		public int? DataRowSpan
		{
			get { return LastDataRowIndex - FirstRowIndex; }
		}

		#endregion
		#region Custom Methods

		/// <summary>
		/// Populates the table with the data from the specifeid <see cref="DataTable"/> in
		/// the passed <see cref="DataSet"/>, inserting worksheet rows as required and mapping
		/// data table columns names or captions to worksheet table column names.
		/// </summary>
		/// <param name="dataSet">A <see cref="DataSet"/> containing data to inject into the worksheet table.</param>
		/// <param name="table">A <see cref="string"/> that specifies the name of the table from which to source data.</param>
		/// <remarks>
		/// Each item in the DataTable is used to populate a new table row. The columns in
		/// the DataTable are mapped to the columns in the worksheet table by name. To allow
		/// greater flexibility, DataTable column captions (defaulting to column names) are
		/// used to indicate worksheet column mappings. Unresolved mappings are ignored.
		/// </remarks>
		public void Fill(
			DataSet dataSet,
			string table)
		{
			if (dataSet == null) throw new ArgumentNullException("dataSet");
			if (string.IsNullOrEmpty(table)) throw new ArgumentNullException("table");

			Fill(dataSet.Tables[table]);
		}

		/// <summary>
		/// Populates the table with the data in the passed <see cref="DataTable"/>, inserting worksheet
		/// rows as required and mapping data table columns names or captions to worksheet table column names.
		/// </summary>
		/// <param name="table">A <see cref="DataTable"/> containing data to inject into the worksheet table.</param>
		/// <remarks>
		/// Each item in the DataTable is used to populate a new table row. The columns in
		/// the DataTable are mapped to the columns in the worksheet table by name. To allow
		/// greater flexibility, DataTable column captions (defaulting to column names) are
		/// used to indicate worksheet column mappings. Unresolved mappings are ignored.
		/// </remarks>
		public void Fill(
			DataTable table)
		{
			if (table == null) throw new ArgumentNullException("table");

			Fill(table.DefaultView);
		}

		/// <summary>
		/// Populates the table with data from the passed <see cref="DataView"/>, inserting worksheet
		/// rows as required and mapping data table columns names or captions to worksheet table column names.
		/// </summary>
		/// <param name="view">A <see cref="DataView"/> containing data to inject into the worksheet table.</param>
		/// <remarks>
		/// Each item in the DataView is used to populate a new table row. The columns in
		/// the DataView are mapped to the columns in the worksheet table by name. To allow
		/// greater flexibility, DataView column captions (defaulting to column names) are
		/// used to indicate worksheet column mappings. Unresolved mappings are ignored.
		/// </remarks>
		public void Fill(
			DataView view)
		{
			if (view == null) throw new ArgumentNullException("view");

			Extend(view.Count);
			var origin = LastDataRowIndex.Value - FirstRowIndex.Value - view.Count;
			var index = 0;

			foreach (DataRowView row in view)
			{
				foreach (DataColumn dataColumn in view.Table.Columns)
				{
					var column = TableColumns.TableColumns[dataColumn.Caption];

					if (column != null)
					{
						SetCellValue(new GridReference(index + origin, column.Index), row[dataColumn.ColumnName]);
					}
				}

				index++;
			}
		}

		/// <summary>
		/// Populates the table with the data in the passed object array, inserting
		/// worksheet rows as required and assigning data items to cells based on
		/// the column name mappings in the accompanying string array.
		/// </summary>
		/// <param name="names">An array of strings representing the mappings of row data items to column names.</param>
		/// <param name="data">An array of arrays representing rows and their contained cell values.</param>
		/// <remarks>
		/// Each item in the outer array is used to populate a new table row. Each item in
		/// the inner array is used to populate a cell within the newly created row. It is
		/// advisable, but not essential, that the number of items in every inner array is
		/// consistent and matches the number of items in the accompanying string array.
		/// Every name specified in the string array must match a table column name, but it
		/// is not essential to populate all table columns. Data items in the inner array will be
		/// applied sequentially to the available mappings in the names array. Surplus data items
		/// will be ignored. A shortage of data items in the inner array will result in empty cells
		/// within the group of mapped columns.
		/// </remarks>
		public void Fill(
			string[] names,
			object[][] data)
		{
			if (names == null) throw new ArgumentNullException("names");
			if (data == null) throw new ArgumentNullException("data");
	
			Extend(data.Length);
			var origin = LastDataRowIndex.Value - FirstRowIndex.Value - data.Length;

			for (var row = 0; row < data.Length; row++)
			{
				for (var index = 0; index < names.Length; index++)
				{
					if (data[row].Length > index)
					{
						var column = TableColumns.TableColumns[names[index]];

						if (column != null)
						{
							SetCellValue(new GridReference(row + origin, column.Index), data[row][index]);
						}
					}
				}
			}
		}

		/// <summary>
		/// Populates the table with the the data in the passed
		/// object array, inserting worksheet rows as required.
		/// </summary>
		/// <param name="data">An array of arrays representing rows and their contained cell values.</param>
		/// <remarks>
		/// Each item in the outer array is used to populate a new table row. Each item in
		/// the inner array is used to populate a cell within the newly created row. It is
		/// advisable, but not essential, that the number of items in every inner array is
		/// consistent and matches the number of data columns (ignoring calculated columns)
		/// in the table. Data items in the inner array will be applied sequentially to the available
		/// columns in the table. Surplus data items will be ignored. A shortage of data items
		/// in the inner array will result in empty cells at the right side of the table row.
		/// </remarks>
		public void Fill(
			object[][] data)
		{
			if (data == null) throw new ArgumentNullException("data");

			Extend(data.Length);

            var origin = LastDataRowIndex.Value - FirstRowIndex.Value - data.Length;

			for(var row = 0; row < data.Length; row++)
			{
				var index = 0;

				foreach (var column in GetNonCalculatedColumns())
				{
					if (data[row].Length > index)
					{
						SetCellValue(new GridReference(row + origin, column.Index), data[row][index++]);
					}
				}
			}
		}

		/// <summary>
		/// Extends the table by the specified number of rows
		/// by inserting empty rows into the underlying worksheet.
		/// </summary>
		/// <param name="rowCount">The number of new empty rows to create.</param>
		/// <remarks>
		/// If the table is empty when this method is invoked (i.e. it contains only
		/// the mandatory single empty row), the number of added rows is reduced by one,
		/// so that the previous empty row will be consumed by a subsequent data injection.
		/// </remarks>
		public void Extend(
			int rowCount)
		{
			if (rowCount < 0) throw new ArgumentOutOfRangeException("rowCount");
			if (rowCount == 0) return;

			if (IsEmpty())
			{
				rowCount--;
			}

			var anchor = LastDataRowIndex.Value;

			if ((TotalsRowCount ?? 0) == 0)
			{
				// Tables with no totals row must be temporarily expanded and subsequently contracted
				// to enable them to qualify for notification of the underlying worksheet row insertion.
				LastRowIndex++;
				Worksheet.InsertRows(anchor + 1, rowCount);
				LastRowIndex--;
			}
			else
			{
				Worksheet.InsertRows(anchor + 1, rowCount);
			}

			for (var i = LastDataRowIndex.Value - rowCount; i < LastDataRowIndex.Value; i++)
			{
				PropagateCalculatedColumnFormulae(i + 1);
			}
		}

		/// <summary>
		/// Gets the value of the cell at the specified table-relative coordinates, converted to the specified data type.
		/// </summary>
		/// <typeparam name="T">One of the supported data types (see <see cref="CellWrapper"/>).</typeparam>
		/// <param name="rowIndex">The zero-based table-relative index of the row containing the target cell.</param>
		/// <param name="columnIndex">The zero-based table-relative index of the column containing the target cell.</param>
		/// <returns>The value of the specified cell converted to specified data type.</returns>
		/// <remarks>This method overload is deprecated. You should use the equivalent overload that accepts <see cref="GridReference"/>.</remarks>
		[Obsolete]
		public T GetCellValue<T>(
			int rowIndex,
			int columnIndex)
		{
			if (rowIndex < 0) throw new ArgumentOutOfRangeException("rowIndex");
			if (columnIndex < 0) throw new ArgumentOutOfRangeException("columnIndex");
			return Worksheet.GetCellValue<T>(new GridReference(rowIndex, columnIndex));
		}

		/// <summary>
		/// Gets the value of the cell at the specified table-relative coordinates, converted to the specified data type.
		/// </summary>
		/// <typeparam name="T">One of the supported data types (see <see cref="CellWrapper"/>).</typeparam>
		/// <param name="address">The grid address of the target cell.</param>
		/// <returns>The value of the specified cell converted to specified data type.</returns>
		/// <remarks>
		/// Unlike the equivalent method of the <see cref="WorksheetWrapper"/> class, this method accepts
		/// coordinates that are relative to the data origin of the table. Specifically, the coordinates
		/// 0, 0 represent the first column in the first data row (ignoring the header row) of the table.
		/// </remarks>
		public T GetCellValue<T>(
			GridReference address)
		{
            if (address.RowIndex > DataRowSpan) throw new ArgumentOutOfRangeException("address");
            if (address.ColumnIndex > TableColumns.TableColumns.Count - 1) throw new ArgumentOutOfRangeException("address");

			var target = new GridReference(FirstRowIndex.Value + 1 + address.RowIndex, FirstColumnIndex.Value + address.ColumnIndex);
			return Worksheet.GetCellValue<T>(target);
		}

		/// <summary>
		/// Sets the content of the cell at the specified table-relative coordinates to the passed value.
		/// </summary>
		/// <param name="rowIndex">The zero-based table-relative index of the row containing the target cell.</param>
		/// <param name="columnIndex">The zero-based table-relative index of the column containing the target cell.</param>
		/// <param name="value">The value to be assigned to the cell.</param>
		/// <remarks>This method overload is deprecated. You should use the equivalent overload that accepts <see cref="GridReference"/>.</remarks>
		[Obsolete]
		public void SetCellValue(
			int rowIndex,
			int columnIndex,
			object value)
		{
			if (rowIndex < 0) throw new ArgumentOutOfRangeException("rowIndex");
			if (columnIndex < 0) throw new ArgumentOutOfRangeException("columnIndex");

			Worksheet.SetCellValue(new GridReference(rowIndex, columnIndex), value);
		}

		/// <summary>
		/// Sets the content of the cell at the specified table-relative coordinates to the passed value.
		/// </summary>
		/// <param name="address">The grid address of the target cell.</param>
		/// <param name="value">The value to be assigned to the cell.</param>
		/// <remarks>
		/// Unlike the equivalent method of the <see cref="WorksheetWrapper"/> class, this method accepts
		/// coordinates that are relative to the data origin of the table. Specifically, the coordinates
		/// 0, 0 represent the first column in the first data row (ignoring the header row) of the table.
		/// </remarks>
		public void SetCellValue(
			GridReference address,
			object value)
		{
            if (address.RowIndex > DataRowSpan) throw new ArgumentOutOfRangeException("address");
			if (address.ColumnIndex > TableColumns.TableColumns.Count - 1) throw new ArgumentOutOfRangeException("address");

			var target = new GridReference(FirstRowIndex.Value + 1 + address.RowIndex, FirstColumnIndex.Value + address.ColumnIndex);
			Worksheet.SetCellValue(target, value);
		}

		#endregion
		#region Overrides

		/// <summary>
		/// Implements custom initialization functionality.
		/// </summary>
		protected override void OnCustomInitializing()
		{
			CellArea = new CellArea();
		}

		/// <summary>
		/// Implements custom population functionality.
		/// </summary>
		protected override void OnCustomDecomposing()
		{
			CellArea = AddressParser.ParseCellArea(Reference);
		}

		/// <summary>
		/// Establishes relationships with other package content objects.
		/// </summary>
		protected override void OnCustomActivating()
		{
			RegisterAddressDependencies();
		}

		/// <summary>
		/// Implements custom persistence functionality.
		/// </summary>
		protected override void OnCustomRecomposing(
			Table wrapped)
		{
			Reference = CellArea.Range;
		}

		/// <summary>
		/// Implements custom deletion functionality.
		/// </summary>
		protected override void OnCustomRemoved()
		{
			ReleaseAddressDependencies();
		}

		/// <summary>
		/// Returns the set of <see cref="IWrapper"/> instances defined as custom properties.
		/// </summary>
		protected override IEnumerable<IWrapper> GetCustomWrapperMembers()
		{
			yield return CellArea;
		}

		#endregion
		#region Implementation Methods

		/// <summary>
		/// Registers addressable contained objects for notification of address changes.
		/// </summary>
		private void RegisterAddressDependencies()
		{
			WorksheetPart.AddressDependencies.Add(CellArea);

			CellArea.RowsDeleted += HandleRowDeletion;
			CellArea.ColumnsDeleted += HandleColumnDeletion;
			CellArea.ColumnsInserted += HandleColumnInsertion;
			CellArea.RangeDeleted += HandleRangeDeletion;
		}

		/// <summary>
		/// Releases previously registered contained objects from notification of address changes.
		/// </summary>
		private void ReleaseAddressDependencies()
		{
			if (WorksheetPart.AddressDependencies.Contains(CellArea))
			{
				WorksheetPart.AddressDependencies.Remove(CellArea);
			}

			CellArea.RowsDeleted -= HandleRowDeletion;
			CellArea.ColumnsDeleted -= HandleColumnDeletion;
			CellArea.ColumnsInserted -= HandleColumnInsertion;
			CellArea.RangeDeleted -= HandleRangeDeletion;
		}

		/// <summary>
		/// Copies the defined calculated column formulae to the cells
		/// associated with the specified worksheet-relative row index.
		/// </summary>
		private void PropagateCalculatedColumnFormulae(
			int row)
		{
			foreach (var column in GetCalculatedColumns())
			{
				Worksheet.SetCellFormula(new GridReference(row, column.WorksheetColumnIndex), column.CalculatedColumnFormula.Value);
			}
		}

		/// <summary>
		/// Returns the set of <see cref="TableColumnWrapper"/> that represent calculated columns.
		/// </summary>
		private IEnumerable<TableColumnWrapper> GetCalculatedColumns()
		{
			foreach (var column in TableColumns.TableColumns)
			{
				if (column.CalculatedColumnFormula.Value != null)
				{
					yield return column;
				}
			}
		}

		/// <summary>
		/// Returns the set of <see cref="TableColumnWrapper"/> that represent non-calculated columns.
		/// </summary>
		private IEnumerable<TableColumnWrapper> GetNonCalculatedColumns()
		{
			foreach (var column in TableColumns.TableColumns)
			{
				if (column.CalculatedColumnFormula.Value == null)
				{
					yield return column;
				}
			}
		}

		/// <summary>
		/// Returns whether this table instance is unpopulated with data rows.
		/// </summary>
		private bool IsEmpty()
		{
			if (DataRowSpan == 1)
			{
				foreach (var column in GetNonCalculatedColumns())
				{
                    if (GetCellValue<string>(new GridReference(0, column.Index)) != null)
					{
						return false;
					}
				}

				return true;
			}

			// A table is never empty if it has more than one row.
			return false;
		}

		/// <summary>
		/// Handles the row deletion event of <see cref="CellArea"/>.
		/// </summary>
		private void HandleRowDeletion(
			object sender,
			RangeResizeEventArgs e)
		{
			if (e.Index == 0 && e.Index + e.Count <= LastDataRowIndex)
			{
				throw new InvalidOperationException("Row deletion cannot span only the upper boundary of a table.");
			}

			var dataRows = (int)(LastRowIndex - (Wrapped.TotalsRowCount ?? 0) - FirstRowIndex);

			if (dataRows < 1)
			{
				CellArea.EndCell.RowIndex += 1 - dataRows;
			}
		}

		/// <summary>
		/// Handles the column deletion event of <see cref="CellArea"/>.
		/// </summary>
		private void HandleColumnDeletion(
			object sender,
			RangeResizeEventArgs e)
		{
			for (var i = 0; i < e.Count; i++)
			{
				TableColumns.TableColumns.RemoveAt(e.Index);
			}
		}

		/// <summary>
		/// Handles the column insertion event of <see cref="CellArea"/>.
		/// </summary>
		private void HandleColumnInsertion(
			object sender,
			RangeResizeEventArgs e)
		{
			for (var i = e.Index; i < e.Index + e.Count; i++)
			{
//ToDo: fix required. See Developer Guide section "IAddressDependency Sequence Problems"
				TableColumns.TableColumns.Add(i);				
			}
		}

		/// <summary>
		/// Handles the range deletion event of <see cref="CellArea"/>.
		/// </summary>
		private void HandleRangeDeletion(
			object sender,
			EventArgs e)
		{
			Remove();
		}

		#endregion
		#region Static Methods


		#endregion
	}
}
